package uy.org.softtech.cssuite.servicios.impl;

import static org.apache.commons.lang.ArrayUtils.isEmpty;
import static org.springframework.util.Assert.notNull;
import static org.springframework.util.CollectionUtils.isEmpty;
import static org.springframework.util.StringUtils.hasText;
import static uy.org.softtech.cssuite.entidad.Entidad.CANTIDAD_REGISTROS;
import static uy.org.softtech.cssuite.helpers.HelperFechaHora.fechaActual;

import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import uy.org.softtech.cssuite.core.Servicio;
import uy.org.softtech.cssuite.entidad.usuario.Rol;
import uy.org.softtech.cssuite.entidad.usuario.Usuario;
import uy.org.softtech.cssuite.excepciones.EntidadNoActualizadaException;
import uy.org.softtech.cssuite.excepciones.EntidadNoCreadaException;
import uy.org.softtech.cssuite.excepciones.EntidadNoEliminadaException;
import uy.org.softtech.cssuite.excepciones.EntidadNoEncontradaException;
import uy.org.softtech.cssuite.excepciones.NotificacionNoEnviadaException;
import uy.org.softtech.cssuite.excepciones.TokenExpiradoException;
import uy.org.softtech.cssuite.repositorios.RepositorioRoles;
import uy.org.softtech.cssuite.repositorios.RepositorioUsuarios;
import uy.org.softtech.cssuite.servicios.ServiciosCorreoElectronico;
import uy.org.softtech.cssuite.servicios.ServiciosUsuarios;
import uy.org.softtech.cssuite.web.entidad.AgenteUsuario;
import uy.org.softtech.cssuite.web.entidad.DetalleUsuario;
import uy.org.softtech.cssuite.web.entidad.SesionUsuario;

import com.hazelcast.core.Hazelcast;
import com.hazelcast.core.IMap;

/**
 * Clase que implementa {@link ServiciosUsuarios}.
 * 
 * @author fabian.lobo
 * @see ServiciosUsuarios
 */
@Service(value = "servicios-usuarios")
public class ServiciosUsuariosImpl extends Servicio implements ServiciosUsuarios {
	
	private Logger log = Logger.getLogger(getClass());
	
	@Resource(name = "repositorio-roles" )
	private RepositorioRoles repositorioRoles;
	
	@Resource(name = "repositorio-usuarios" )
	private RepositorioUsuarios repositorioUsuarios;
	
	@SuppressWarnings({ "deprecation", "rawtypes" })
	private IMap tokens = Hazelcast.getMap("tokens");
	
	@Resource(name = "servicios-correo" )
	private ServiciosCorreoElectronico serviciosCorreoElectronico;
	
	@Resource(name = "registro-sesiones")
	private SessionRegistry registroSesiones;
	
	/**
	 * Método para verificar que todos los parámetros han sido seteados correctamente.
	 * 
	 * @author fabian.lobo
	 */
	@PostConstruct
	public void inicializacion(){
		notNull(repositorioRoles(), "La propiedad [repositorioRoles] de [" + getClass().getName() + "] no ha sido establecida.");
		notNull(repositorioUsuarios(), "La propiedad [repositorioUsuarios] de [" + getClass().getName() + "] no ha sido establecida.");
		notNull(tokens(), "La propiedad [tokens] de [" + getClass().getName() + "] no ha sido establecida.");
		notNull(serviciosCorreoElectronico(), "La propiedad [serviciosCorreoElectronico] de [" + getClass().getName() + "] no ha sido establecida.");
		notNull(registroSesiones(), "La propiedad [registroSesiones] de [" + getClass().getName() + "] no ha sido establecida.");
		
		log.info("Implementación de los servicios relacionados a los usuarios ha sido inicializado correctamente.");
	}

	/**
	 * Busca un usuario registrado en la aplicación usando su correo electrónico como identificador de usuario.
	 * 
	 * @param username - Dirección de correo electrónico del usuario
	 * @author fabian.lobo
	 */
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		notNull(username, "La dirección de correo electrónico del usuario no puede ser nula");
		DetalleUsuario detalles = null;

		//Buscamos el usuario por la dirección de correo electrónico.
		Usuario usuario = null;
		try {
			usuario = repositorioUsuarios().buscarUsuarioPorEmail(username);

			//Actualizamos la fecha de último login
			usuario.setUltimoLogin(fechaActual());
			usuario = repositorioUsuarios().actualizar(usuario, SERVICIO + "#loadUserByUsername");

			detalles = new DetalleUsuario(usuario);
		} catch (EntidadNoEncontradaException unee) {
			throw new UsernameNotFoundException(unee.getMessage());
		} catch (EntidadNoActualizadaException enae) {
			log.error(enae);
		} catch (Throwable e) {
			log.error(e);
		}
		return detalles;
	}

	@Override
	@SuppressWarnings("unchecked")
	public void notificarCambioCredenciales(String email, Locale locale) throws EntidadNoEncontradaException, NotificacionNoEnviadaException {
		//1. Generar un token dinámico
		String token = UUID.randomUUID().toString();
		tokens().put(email, token);
		tokens().put(token, email);
		log.debug("Token [" + token + "] generado para el usuario " + email);
		
		//2. Enviar correo electrónico
		Usuario usuario = repositorioUsuarios().buscarUsuarioPorEmail(email);
		serviciosCorreoElectronico().notificarSolicitudCambioCredenciales(usuario, token, locale);
	}
	
	@Override
	public String buscarUsuarioPorToken(String token) throws TokenExpiradoException {
		String usuario = (String) tokens().get(token);
		if(!hasText(usuario)) {
			throw new TokenExpiradoException(token);
		}
		return usuario;
	}
	
	@Override
	public Usuario buscarUsuario(String usuario) throws EntidadNoEncontradaException {
		return repositorioUsuarios().buscarUsuarioPorEmail(usuario);
	}
	
	@Override
	public Usuario actualizar(Usuario usuario, String servicio) throws EntidadNoActualizadaException {
		return repositorioUsuarios().actualizar(usuario, servicio);
	}
	
	@Override
	public void remuevoToken(String email) {
		String token = (String) tokens().get(email);
		if(hasText(token)) {
			tokens().remove(email);
			tokens().remove(token);
		}
	}
	
	@Override
	public Usuario buscarUsuario(Long id) throws EntidadNoEncontradaException {
		return repositorioUsuarios().buscar(id);
	}
	
	@Override
	public Set<SesionUsuario> listarSesiones(Locale locale) {
		Set<SesionUsuario> lista = new LinkedHashSet<SesionUsuario>();
		try {
			List<Object> usuariosLogueados = registroSesiones().getAllPrincipals();
			for(Object usuario : usuariosLogueados) {
				List<SessionInformation> sesiones = registroSesiones().getAllSessions(usuario, false);
				for(SessionInformation sesion : sesiones){
					SesionUsuario item = new SesionUsuario();
					item.setSesion(sesion.getSessionId());
					item.setUltimoPedido(sesion.getLastRequest(), locale);
					
					if(usuario instanceof DetalleUsuario) {
						DetalleUsuario detalle = (DetalleUsuario) usuario;
						item.setDireccion(detalle.getDireccion());
						
						//Detalles del navegador y sistema operativo
						AgenteUsuario agente = new AgenteUsuario(detalle.getNavegador());
						item.setNavegador(agente.getNavegador().getNombre());
						item.setPlataforma(agente.getPlataforma().getNombre());
						
						//Lenguaje del navegador
						String lenguajes = detalle.getLenguaje();
						if (hasText(lenguajes)) {
							String[] languages = lenguajes.split(",");
							if (!isEmpty(languages)) {
								Locale userLocale = new Locale(languages[0]);
								item.setLenguaje(userLocale.getDisplayName(locale));
							}
						}

						//Detalles del usuario logueado
						item.setUsuario(detalle.getUsuario().getUsuario());
						item.setHoraComienzo(detalle.getUsuario().getUltimoLogin(), locale);
					}
					lista.add(item);
				}				
			}
			
			//Máxima cantidad de registros para optimizar performance
			if(lista.size() > CANTIDAD_REGISTROS) {
				Set<SesionUsuario> filtro = new LinkedHashSet<SesionUsuario>();
				Iterator<SesionUsuario> iterador = lista.iterator();
				for(int idx = 0; idx < CANTIDAD_REGISTROS; idx++) {
					filtro.add(iterador.next());
				}
				return filtro;
			}
		} catch (Throwable e) {
			log.error(e);
		}		
		return lista;
	}
	
	@Override
	public Integer usuariosConectados() {
		return registroSesiones().getAllPrincipals().size();
	}
	
	@Override
	public void cerrarSesion(String sesion) {
		SessionInformation sesionUsuario = registroSesiones().getSessionInformation(sesion);
		if(sesionUsuario != null && !sesionUsuario.isExpired()){
			sesionUsuario.expireNow();
		}
		registroSesiones().removeSessionInformation(sesion);		
	}
	
	@Override
	public boolean validaSesion(String sesion) {
		SessionInformation sesionUsuario = registroSesiones().getSessionInformation(sesion);
		return sesionUsuario != null && !sesionUsuario.isExpired();
	}
	
	@Override
	public Set<Usuario> listarUsuarios() {
		return repositorioUsuarios().listarUsuarios();
	}
	
	@Override
	public Set<Rol> listarRoles() {
		return repositorioRoles().listarRoles();
	}
	
	@Override
	public Rol buscarRol(Long id) throws EntidadNoEncontradaException {
		return repositorioRoles().buscar(id);
	}

	@Override
	public Usuario crear(Usuario usuario, String servicio) throws EntidadNoCreadaException {
		return repositorioUsuarios().crear(usuario, servicio);
	}
	
	@Override
	public void bloquearUsuario(Long id) throws EntidadNoEncontradaException, EntidadNoActualizadaException {
		//1. Actualizamos el registro en la base de datos
		Usuario usuario = buscarUsuario(id);
		usuario.setBloqueado(true);
		actualizar(usuario, SERVICIO);
		
		//2. Cerramos cualquier sesión que el usuario tenga abierta
		List<SessionInformation> sesiones = registroSesiones().getAllSessions(usuario.getUsuario(), false);
		if(!isEmpty(sesiones)) {
			for(SessionInformation sesion : sesiones) {
				cerrarSesion(sesion.getSessionId());
			}
		}
	}

	@Override
	public void desbloquearUsuario(Long id) throws EntidadNoEncontradaException, EntidadNoActualizadaException {
		Usuario usuario = buscarUsuario(id);
		usuario.setBloqueado(false);
		actualizar(usuario, SERVICIO);
	}

	@Override
	public void eliminarUsuario(Long id) throws EntidadNoEncontradaException, EntidadNoEliminadaException {
		//1. Actualizamos el registro en la base de datos
		Usuario usuario = buscarUsuario(id);
		usuario.setEliminado(true);
		try {
			actualizar(usuario, SERVICIO);
		} catch (EntidadNoActualizadaException e) {
			throw new EntidadNoEliminadaException(usuario);
		}
		
		//2. Cerramos cualquier sesión que el usuario tenga abierta
		List<SessionInformation> sesiones = registroSesiones().getAllSessions(usuario.getUsuario(), false);
		if(!isEmpty(sesiones)) {
			for(SessionInformation sesion : sesiones) {
				cerrarSesion(sesion.getSessionId());
			}
		}
	}
	
	/*
	 *  GETTERS
	 */
	public RepositorioRoles repositorioRoles() {
		return repositorioRoles;
	}
	public RepositorioUsuarios repositorioUsuarios() {
		return repositorioUsuarios;
	}
	@SuppressWarnings("rawtypes")
	public IMap tokens() {
		return tokens;
	}
	public ServiciosCorreoElectronico serviciosCorreoElectronico() {
		return serviciosCorreoElectronico;
	}
	public SessionRegistry registroSesiones() {
		return registroSesiones;
	}

}
